package iit.projects.etaxi.model.service;

import iit.projects.etaxi.model.dao.DriverDao;
import iit.projects.etaxi.model.dao.MessageDao;
import iit.projects.etaxi.model.dao.ReservationDao;
import iit.projects.etaxi.model.dao.VehicleDao;
import iit.projects.etaxi.model.dto.ReservationSCDO;
import iit.projects.etaxi.model.entity.Driver;
import iit.projects.etaxi.model.entity.Message;
import iit.projects.etaxi.model.entity.Reservation;
import iit.projects.etaxi.model.entity.SendingSms;
import iit.projects.etaxi.model.entity.Vehicle;
import iit.projects.etaxi.model.exception.DriverNotFoundException;
import iit.projects.etaxi.model.exception.DuplicateReservationException;
import iit.projects.etaxi.model.exception.DuplicateSequenceException;
import iit.projects.etaxi.model.exception.ReservationNotFoundException;
import iit.projects.etaxi.util.Constants;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly=true)
public class ReservationServiceImpl implements ReservationService {
	
	final Logger log = LoggerFactory.getLogger(ReservationServiceImpl.class);
	
	private ReservationDao reservationDao;
	private VehicleDao vehicleDao;
	private MessageDao messageDao;
	private DriverDao driverDao;
	private SequenceGenerator sequenceGenerator;
	
	private int reservationProcessingLimit;
	private int smsDriversCount;
	private int noreplyWarningLimit;

	@Override
	@Transactional(readOnly = false, 
			propagation=Propagation.REQUIRES_NEW, 
			rollbackFor = {DuplicateReservationException.class,DuplicateSequenceException.class})
	public Reservation createReservation(Reservation reservation)
			throws DuplicateReservationException, DuplicateSequenceException {
		
		reservation.setCode(sequenceGenerator.getGeneratedCode(Constants.SequenceNames.RESERVATION));
		reservation.setLastProcessedTime(new Date());
		return reservationDao.createReservation(reservation);
	}

	@Override
	public Reservation findByCode(String code)
			throws ReservationNotFoundException {
		
		return reservationDao.findByCode(code);
	}
	
	@Override
	@Transactional(readOnly = false, 
			propagation=Propagation.REQUIRES_NEW)
	public Reservation updateReservation(Reservation reservation){
		return reservationDao.updateReservation(reservation);
	}
	
	@Override
	@Transactional(readOnly=false, propagation=Propagation.REQUIRES_NEW)
	public void processReservations(){
		
		Calendar calendar = Calendar.getInstance();
		Date currentTime = calendar.getTime();
		
		if(log.isDebugEnabled()){
			log.debug("Going to process Reservations at " + currentTime);
		}
		
		Date startTime = currentTime;
		calendar.add(Calendar.MINUTE, reservationProcessingLimit);
		Date endTime = calendar.getTime();
		
		//Find New Reservations which are reservationProcessingLimit minutes ahead of the current time.
		List<Reservation> reservations = reservationDao.findReservationsByStatusAndTime(Constants.ReservationStatus.NEW,
																						startTime, endTime);
		
		if(reservations != null && !reservations.isEmpty()){
			
			if(log.isDebugEnabled()){
				log.debug("Found " + reservations.size() + " Reservations");
			}
			
			List<Message> messages = new ArrayList<Message>();
			for(Reservation reservation : reservations){
				
				//If the customer who made the reservation has now been suspended
				if(reservation.getCustomer() != null && reservation.getCustomer().isSuspended()){
					continue;
				}
				
				List<Driver> drivers = findDriversForReservation(reservation);
				
				reservation.setStatus(Constants.ReservationStatus.PROCESSED);
				reservation.setLastProcessedTime(new Date());
				
				if(drivers != null && !drivers.isEmpty()){
					for(Driver driver : drivers){
						SendingSms sms = new SendingSms();
						
						SimpleDateFormat dateFormat = new SimpleDateFormat("dd:MM:yy HH:mm");
						//Date pickupTime = dateFormat.format(reservation.getPickUpTime());
						
						
						sms.setMessage(reservation.getCode() + " | " 
								+ reservation.getPickupAddress() + " | " 
								+ dateFormat.format(reservation.getPickUpTime()) + " | "
								+ reservation.getToSubCity().getName());
						sms.setRecepient(driver.getContactNumber());
						sms.setReservation(reservation);
						sms.setType(Constants.SmsTypes.REQUEST);
						reservation.getSms().add(sms);
					}
				}
				else{
					log.info("No Drivers available for Reservation " + reservation.getCode());
					Message message = new Message();
					message.setMessage("No Drivers available for assigning Reservation " + reservation.getCode());
					message.setDate(currentTime);
					messages.add(message);
					reservation.setStatus(Constants.ReservationStatus.UNASSIGNED);
				}
				reservationDao.updateReservation(reservation);
			}
			messageDao.saveMessages(messages);
		}
	}
	
	private List<Driver> findDriversForReservation(Reservation reservation){
		
		Calendar ending = new GregorianCalendar();
		ending.setTime(reservation.getPickUpTime());
		
		Calendar starting = new GregorianCalendar();
		starting.setTime(ending.getTime());
		starting.add(Calendar.HOUR, -1);
		
		List<Driver> drivers = null;
		List<Vehicle> vehilcesInProximity = vehicleDao.findAvailableVehiclesInProximity(reservation.getFromSubCity(),
				starting.getTime(), ending.getTime());
		
		if(vehilcesInProximity != null && !vehilcesInProximity.isEmpty()){
			drivers = new ArrayList<Driver>();
			for(Vehicle vehicle : vehilcesInProximity){
				
				//Check whether the seating and luggage capacity of the vehicle
				//matches the seating and luggage capacity requested
				if(vehicle.getSeatingCapcity() >= reservation.getSeatingCapacity() &&
				   vehicle.getLuggageCapacity() >= reservation.getLuggageCapacity()){
					
					drivers.add(vehicle.getDriver());
				}
				
				if(drivers.size() == smsDriversCount){
					return drivers;
				}
			}
		}
		
		List<Vehicle> availableVehicles = vehicleDao.findAvailableVehicles();
		if(availableVehicles != null && !availableVehicles.isEmpty()){
			if(drivers == null){
				drivers = new ArrayList<Driver>();
			}
			for(Vehicle vehicle : availableVehicles){
				
				//Check whether the seating and luggage capacity of the vehicle
				//matches the seating and luggage capacity requested
				if(vehicle.getSeatingCapcity() >= reservation.getSeatingCapacity() &&
				   vehicle.getLuggageCapacity() >= reservation.getLuggageCapacity()){
					
					drivers.add(vehicle.getDriver());
				}
				
				if(drivers.size() == smsDriversCount){
					return drivers;
				}
			}
		}
		return drivers;
	}
	
	@Override
	public List<Reservation> searchReservations(ReservationSCDO reservationSCDO){
		Calendar calendar = Calendar.getInstance();
		if(reservationSCDO.getStartingDate() != null){
			calendar.setTime(reservationSCDO.getStartingDate());
			calendar.set(Calendar.HOUR, 0);
			calendar.set(Calendar.MINUTE, 0);
			calendar.set(Calendar.SECOND, 0);
			
			reservationSCDO.setStartingDate(calendar.getTime());
		}
		if(reservationSCDO.getEndingDate() != null){
			calendar.setTime(reservationSCDO.getEndingDate());
			calendar.set(Calendar.HOUR, 23);
			calendar.set(Calendar.MINUTE, 59);
			calendar.set(Calendar.SECOND, 59);
			
			reservationSCDO.setEndingDate(calendar.getTime());
		}
		
		return reservationDao.searchReservations(reservationSCDO);
		
	}
	
	@Override
	@Transactional(readOnly=false, propagation=Propagation.REQUIRES_NEW)
	public Reservation requestDriverForReservation(Reservation reservation, Driver driver){
		
		SimpleDateFormat dateFormat = new SimpleDateFormat("dd:MM:yy HH:mm");
		
		SendingSms sms = new SendingSms();
		sms.setMessage(reservation.getCode() + " | " 
				+ reservation.getPickupAddress() + " | " 
				+ dateFormat.format(reservation.getPickUpTime()) + " | "
				+ reservation.getToSubCity().getName());
		sms.setRecepient(driver.getContactNumber());
		sms.setReservation(reservation);
		sms.setType(Constants.SmsTypes.REQUEST);
		
		reservation.setStatus(Constants.ReservationStatus.PROCESSED);
		reservation.setLastProcessedTime(new Date());
		reservation.getSms().add(sms);
		
		return reservationDao.updateReservation(reservation);
	}
	
	@Override
	@Transactional(readOnly = false, 
			propagation=Propagation.REQUIRES_NEW)
	public void processNoReplySms(){
		
		Date timeNow = new Date();
		
		if(log.isDebugEnabled()){
			log.debug("Going to process No Reply Reservations at " + timeNow);
		}
		
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(timeNow);
		
		calendar.roll(Calendar.MINUTE, -noreplyWarningLimit);
		
		List<Reservation> reservations = 
			reservationDao.findReservationsBefore(Constants.ReservationStatus.PROCESSED,
												  calendar.getTime());
		
		if(reservations != null && !reservations.isEmpty()){
			List<Message> messages = new ArrayList<Message>();
			
			for(Reservation reservation : reservations){
				
				Set<SendingSms> sentSms = reservation.getSms();
				if(sentSms != null && !sentSms.isEmpty()){
					reservation.setLastProcessedTime(timeNow);
					reservation.setStatus(Constants.ReservationStatus.UNASSIGNED);
					
					StringBuffer stringBuffer = new StringBuffer("No Response from ");
					
					int index = 1;
					for(SendingSms sms : sentSms){
						try {
							Driver driver = driverDao.findDriverByContactNumber(sms.getRecepient());
							stringBuffer.append(driver.getName());
							if(index != sentSms.size()){
								stringBuffer.append(", ");
							}
							else{
								stringBuffer.append(" ");
							}
							index++;
						} catch (DriverNotFoundException e) {
							log.error("No Driver found for Phone Number " + sms.getRecepient());
						}
					}
					
					stringBuffer.append("for Reservation " + reservation.getCode());
					Message message = new Message();
					message.setMessage(stringBuffer.toString());
					message.setDate(timeNow);
					messages.add(message);
				}
			}
			
			messageDao.saveMessages(messages);
		}
	}
	
	
	public void setReservationDao(ReservationDao reservationDao) {
		this.reservationDao = reservationDao;
	}

	public void setVehicleDao(VehicleDao vehicleDao) {
		this.vehicleDao = vehicleDao;
	}

	public void setMessageDao(MessageDao messageDao) {
		this.messageDao = messageDao;
	}

	public void setReservationProcessingLimit(int reservationProcessingLimit) {
		this.reservationProcessingLimit = reservationProcessingLimit;
	}

	public void setSequenceGenerator(SequenceGenerator sequenceGenerator) {
		this.sequenceGenerator = sequenceGenerator;
	}

	public int getSmsDriversCount() {
		return smsDriversCount;
	}

	public void setSmsDriversCount(int smsDriversCount) {
		this.smsDriversCount = smsDriversCount;
	}

	public int getNoreplyWarningLimit() {
		return noreplyWarningLimit;
	}

	public void setNoreplyWarningLimit(int noreplyWarningLimit) {
		this.noreplyWarningLimit = noreplyWarningLimit;
	}

	public void setDriverDao(DriverDao driverDao) {
		this.driverDao = driverDao;
	}

}
